/*
 * Copyright 2024 The CHANGLEI Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

part of '../px.dart';

/// 像素大小
extension RRectToPx on RRect {
  /// 像素大小
  PxRRect get px {
    return PxRRect._raw(
      top: top.px,
      left: left.px,
      right: right.px,
      bottom: bottom.px,
      tlRadiusX: tlRadiusX.px,
      tlRadiusY: tlRadiusY.px,
      trRadiusX: trRadiusX.px,
      trRadiusY: trRadiusY.px,
      blRadiusX: blRadiusX.px,
    );
  }

  /// 像素大小
  PxRRect toPx(BuildContext context) {
    return PxRRect._raw(
      top: top.toPx(context),
      left: left.toPx(context),
      right: right.toPx(context),
      bottom: bottom.toPx(context),
      tlRadiusX: tlRadiusX.toPx(context),
      tlRadiusY: tlRadiusY.toPx(context),
      trRadiusX: trRadiusX.toPx(context),
      trRadiusY: trRadiusY.toPx(context),
      blRadiusX: blRadiusX.toPx(context),
    );
  }
}

/// Created by changlei on 2024/4/23.
///
/// See [PxRSTransform]
class PxRRect implements _Px<RRect> {
  /// See [RRect.fromLTRBXY]
  const PxRRect.fromLTRBXY(
    PxDouble left,
    PxDouble top,
    PxDouble right,
    PxDouble bottom,
    PxDouble radiusX,
    PxDouble radiusY,
  ) : this._raw(
          top: top,
          left: left,
          right: right,
          bottom: bottom,
          tlRadiusX: radiusX,
          tlRadiusY: radiusY,
          trRadiusX: radiusX,
          trRadiusY: radiusY,
          blRadiusX: radiusX,
          blRadiusY: radiusY,
          brRadiusX: radiusX,
          brRadiusY: radiusY,
        );

  /// See [RRect.fromLTRBR]
  PxRRect.fromLTRBR(
    PxDouble left,
    PxDouble top,
    PxDouble right,
    PxDouble bottom,
    PxRadius radius,
  ) : this._raw(
          top: top,
          left: left,
          right: right,
          bottom: bottom,
          tlRadiusX: radius.x,
          tlRadiusY: radius.y,
          trRadiusX: radius.x,
          trRadiusY: radius.y,
          blRadiusX: radius.x,
          blRadiusY: radius.y,
          brRadiusX: radius.x,
          brRadiusY: radius.y,
        );

  /// See [RRect.fromRectXY]
  PxRRect.fromRectXY(PxRect rect, PxDouble radiusX, PxDouble radiusY)
      : this._raw(
          top: rect.top,
          left: rect.left,
          right: rect.right,
          bottom: rect.bottom,
          tlRadiusX: radiusX,
          tlRadiusY: radiusY,
          trRadiusX: radiusX,
          trRadiusY: radiusY,
          blRadiusX: radiusX,
          blRadiusY: radiusY,
          brRadiusX: radiusX,
          brRadiusY: radiusY,
        );

  /// See [RRect.fromRectAndCorners]
  PxRRect.fromRectAndRadius(PxRect rect, PxRadius radius)
      : this._raw(
          top: rect.top,
          left: rect.left,
          right: rect.right,
          bottom: rect.bottom,
          tlRadiusX: radius.x,
          tlRadiusY: radius.y,
          trRadiusX: radius.x,
          trRadiusY: radius.y,
          blRadiusX: radius.x,
          blRadiusY: radius.y,
          brRadiusX: radius.x,
          brRadiusY: radius.y,
        );

  /// See [RRect.fromLTRBAndCorners]
  PxRRect.fromLTRBAndCorners(
    PxDouble left,
    PxDouble top,
    PxDouble right,
    PxDouble bottom, {
    PxRadius topLeft = PxRadius.zero,
    PxRadius topRight = PxRadius.zero,
    PxRadius bottomRight = PxRadius.zero,
    PxRadius bottomLeft = PxRadius.zero,
  }) : this._raw(
          top: top,
          left: left,
          right: right,
          bottom: bottom,
          tlRadiusX: topLeft.x,
          tlRadiusY: topLeft.y,
          trRadiusX: topRight.x,
          trRadiusY: topRight.y,
          blRadiusX: bottomLeft.x,
          blRadiusY: bottomLeft.y,
          brRadiusX: bottomRight.x,
          brRadiusY: bottomRight.y,
        );

  /// See [RRect.fromRectAndCorners]
  PxRRect.fromRectAndCorners(
    PxRect rect, {
    PxRadius topLeft = PxRadius.zero,
    PxRadius topRight = PxRadius.zero,
    PxRadius bottomRight = PxRadius.zero,
    PxRadius bottomLeft = PxRadius.zero,
  }) : this._raw(
          top: rect.top,
          left: rect.left,
          right: rect.right,
          bottom: rect.bottom,
          tlRadiusX: topLeft.x,
          tlRadiusY: topLeft.y,
          trRadiusX: topRight.x,
          trRadiusY: topRight.y,
          blRadiusX: bottomLeft.x,
          blRadiusY: bottomLeft.y,
          brRadiusX: bottomRight.x,
          brRadiusY: bottomRight.y,
        );

  const PxRRect._raw({
    this.left = PxDouble.zero,
    this.top = PxDouble.zero,
    this.right = PxDouble.zero,
    this.bottom = PxDouble.zero,
    this.tlRadiusX = PxDouble.zero,
    this.tlRadiusY = PxDouble.zero,
    this.trRadiusX = PxDouble.zero,
    this.trRadiusY = PxDouble.zero,
    this.brRadiusX = PxDouble.zero,
    this.brRadiusY = PxDouble.zero,
    this.blRadiusX = PxDouble.zero,
    this.blRadiusY = PxDouble.zero,
  })  : assert(tlRadiusX >= PxDouble.zero),
        assert(tlRadiusY >= PxDouble.zero),
        assert(trRadiusX >= PxDouble.zero),
        assert(trRadiusY >= PxDouble.zero),
        assert(brRadiusX >= PxDouble.zero),
        assert(brRadiusY >= PxDouble.zero),
        assert(blRadiusX >= PxDouble.zero),
        assert(blRadiusY >= PxDouble.zero);

  /// See [RRect._getValue32]
  Float32List getValue32() {
    final Float32List result = Float32List(12);
    result[0] = left._;
    result[1] = top._;
    result[2] = right._;
    result[3] = bottom._;
    result[4] = tlRadiusX._;
    result[5] = tlRadiusY._;
    result[6] = trRadiusX._;
    result[7] = trRadiusY._;
    result[8] = brRadiusX._;
    result[9] = brRadiusY._;
    result[10] = blRadiusX._;
    result[11] = blRadiusY._;
    return result;
  }

  /// See [RRect.left]
  final PxDouble left;

  /// See [RRect.top]
  final PxDouble top;

  /// See [RRect.right]
  final PxDouble right;

  /// See [RRect.bottom]
  final PxDouble bottom;

  /// See [RRect.tlRadiusX]
  final PxDouble tlRadiusX;

  /// See [RRect.tlRadiusY]
  final PxDouble tlRadiusY;

  /// See [RRect.tlRadius]
  PxRadius get tlRadius => PxRadius.elliptical(tlRadiusX, tlRadiusY);

  /// See [RRect.trRadiusX]
  final PxDouble trRadiusX;

  /// See [RRect.trRadiusY]
  final PxDouble trRadiusY;

  /// See [RRect.trRadius]
  PxRadius get trRadius => PxRadius.elliptical(trRadiusX, trRadiusY);

  /// See [RRect.brRadiusX]
  final PxDouble brRadiusX;

  /// See [RRect.brRadiusY]
  final PxDouble brRadiusY;

  /// See [RRect.brRadius]
  PxRadius get brRadius => PxRadius.elliptical(brRadiusX, brRadiusY);

  /// See [RRect.blRadiusX]
  final PxDouble blRadiusX;

  /// See [RRect.blRadiusY]
  final PxDouble blRadiusY;

  /// See [RRect.blRadius]
  PxRadius get blRadius => PxRadius.elliptical(blRadiusX, blRadiusY);

  /// See [RRect.zero]
  static final PxRRect zero = PxRRect._raw();

  /// See [RRect.shift]
  PxRRect shift(PxOffset offset) {
    return PxRRect._raw(
      left: left + offset.dx,
      top: top + offset.dy,
      right: right + offset.dx,
      bottom: bottom + offset.dy,
      tlRadiusX: tlRadiusX,
      tlRadiusY: tlRadiusY,
      trRadiusX: trRadiusX,
      trRadiusY: trRadiusY,
      blRadiusX: blRadiusX,
      blRadiusY: blRadiusY,
      brRadiusX: brRadiusX,
      brRadiusY: brRadiusY,
    );
  }

  /// See [RRect.inflate]
  PxRRect inflate(PxDouble delta) {
    return PxRRect._raw(
      left: left - delta,
      top: top - delta,
      right: right + delta,
      bottom: bottom + delta,
      tlRadiusX: max(PxDouble.zero, tlRadiusX + delta),
      tlRadiusY: max(PxDouble.zero, tlRadiusY + delta),
      trRadiusX: max(PxDouble.zero, trRadiusX + delta),
      trRadiusY: max(PxDouble.zero, trRadiusY + delta),
      blRadiusX: max(PxDouble.zero, blRadiusX + delta),
      blRadiusY: max(PxDouble.zero, blRadiusY + delta),
      brRadiusX: max(PxDouble.zero, brRadiusX + delta),
      brRadiusY: max(PxDouble.zero, brRadiusY + delta),
    );
  }

  /// See [RRect.deflate]
  PxRRect deflate(PxDouble delta) => inflate(-delta);

  /// See [RRect.width]
  PxDouble get width => right - left;

  /// See [RRect.height]
  PxDouble get height => bottom - top;

  /// See [RRect.outerRect]
  PxRect get outerRect => PxRect.fromLTRB(left, top, right, bottom);

  /// See [RRect.safeInnerRect]
  PxRect get safeInnerRect {
    const kInsetFactor = PxDouble(0.29289321881); // 1-cos(pi/4)

    final leftRadius = max(blRadiusX, tlRadiusX);
    final topRadius = max(tlRadiusY, trRadiusY);
    final rightRadius = max(trRadiusX, brRadiusX);
    final bottomRadius = max(brRadiusY, blRadiusY);

    return PxRect.fromLTRB(
      left + leftRadius * kInsetFactor,
      top + topRadius * kInsetFactor,
      right - rightRadius * kInsetFactor,
      bottom - bottomRadius * kInsetFactor,
    );
  }

  /// See [RRect.middleRect]
  PxRect get middleRect {
    final leftRadius = max(blRadiusX, tlRadiusX);
    final topRadius = max(tlRadiusY, trRadiusY);
    final rightRadius = max(trRadiusX, brRadiusX);
    final bottomRadius = max(brRadiusY, blRadiusY);
    return PxRect.fromLTRB(left + leftRadius, top + topRadius, right - rightRadius, bottom - bottomRadius);
  }

  /// See [RRect.wideMiddleRect]
  PxRect get wideMiddleRect {
    final topRadius = max(tlRadiusY, trRadiusY);
    final bottomRadius = max(brRadiusY, blRadiusY);
    return PxRect.fromLTRB(left, top + topRadius, right, bottom - bottomRadius);
  }

  /// See [RRect.tallMiddleRect]
  PxRect get tallMiddleRect {
    final leftRadius = max(blRadiusX, tlRadiusX);
    final rightRadius = max(trRadiusX, brRadiusX);
    return PxRect.fromLTRB(left + leftRadius, top, right - rightRadius, bottom);
  }

  /// See [RRect.isEmpty]
  bool get isEmpty => left >= right || top >= bottom;

  /// See [RRect.isFinite]
  bool get isFinite => left.isFinite && top.isFinite && right.isFinite && bottom.isFinite;

  /// See [RRect.isRect]
  bool get isRect {
    return (tlRadiusX == PxDouble.zero || tlRadiusY == PxDouble.zero) &&
        (trRadiusX == PxDouble.zero || trRadiusY == PxDouble.zero) &&
        (blRadiusX == PxDouble.zero || blRadiusY == PxDouble.zero) &&
        (brRadiusX == PxDouble.zero || brRadiusY == PxDouble.zero);
  }

  /// See [RRect.isStadium]
  bool get isStadium {
    return tlRadius == trRadius &&
        trRadius == brRadius &&
        brRadius == blRadius &&
        (width <= 2.0.px * tlRadiusX || height <= 2.0.px * tlRadiusY);
  }

  /// See [RRect.isEllipse]
  bool get isEllipse {
    return tlRadius == trRadius &&
        trRadius == brRadius &&
        brRadius == blRadius &&
        width <= 2.0.px * tlRadiusX &&
        height <= 2.0.px * tlRadiusY;
  }

  /// See [RRect.isCircle]
  bool get isCircle => width == height && isEllipse;

  /// See [RRect.shortestSide]
  PxDouble get shortestSide => min(width.abs(), height.abs());

  /// See [RRect.longestSide]
  PxDouble get longestSide => max(width.abs(), height.abs());

  /// See [RRect.hasNaN]
  bool get hasNaN =>
      left.isNaN ||
      top.isNaN ||
      right.isNaN ||
      bottom.isNaN ||
      trRadiusX.isNaN ||
      trRadiusY.isNaN ||
      tlRadiusX.isNaN ||
      tlRadiusY.isNaN ||
      brRadiusX.isNaN ||
      brRadiusY.isNaN ||
      blRadiusX.isNaN ||
      blRadiusY.isNaN;

  /// See [RRect.center]
  PxOffset get center => PxOffset(left + width / 2.0.px, top + height / 2.0.px);

  PxDouble _getMin(PxDouble minValue, PxDouble radius1, PxDouble radius2, PxDouble limit) {
    final PxDouble sum = radius1 + radius2;
    if (sum > limit && sum != 0.0.px) {
      return min(minValue, limit / sum);
    }
    return minValue;
  }

  /// See [RRect.scaleRadii]
  PxRRect scaleRadii() {
    PxDouble scale = 1.0.px;
    scale = _getMin(scale, blRadiusY, tlRadiusY, height);
    scale = _getMin(scale, tlRadiusX, trRadiusX, width);
    scale = _getMin(scale, trRadiusY, brRadiusY, height);
    scale = _getMin(scale, brRadiusX, blRadiusX, width);
    assert(scale >= 0.px);

    if (scale < 1.0.px) {
      return PxRRect._raw(
        top: top,
        left: left,
        right: right,
        bottom: bottom,
        tlRadiusX: tlRadiusX * scale,
        tlRadiusY: tlRadiusY * scale,
        trRadiusX: trRadiusX * scale,
        trRadiusY: trRadiusY * scale,
        blRadiusX: blRadiusX * scale,
        blRadiusY: blRadiusY * scale,
        brRadiusX: brRadiusX * scale,
        brRadiusY: brRadiusY * scale,
      );
    }

    return PxRRect._raw(
      top: top,
      left: left,
      right: right,
      bottom: bottom,
      tlRadiusX: tlRadiusX,
      tlRadiusY: tlRadiusY,
      trRadiusX: trRadiusX,
      trRadiusY: trRadiusY,
      blRadiusX: blRadiusX,
      blRadiusY: blRadiusY,
      brRadiusX: brRadiusX,
      brRadiusY: brRadiusY,
    );
  }

  /// See [RRect.contains]
  bool contains(PxOffset point) {
    if (point.dx < left || point.dx >= right || point.dy < top || point.dy >= bottom) {
      return false;
    } // outside bounding box

    final PxRRect scaled = scaleRadii();

    PxDouble x;
    PxDouble y;
    PxDouble radiusX;
    PxDouble radiusY;
    // check whether point is in one of the rounded corner areas
    // x, y -> translate to ellipse center
    if (point.dx < left + scaled.tlRadiusX && point.dy < top + scaled.tlRadiusY) {
      x = point.dx - left - scaled.tlRadiusX;
      y = point.dy - top - scaled.tlRadiusY;
      radiusX = scaled.tlRadiusX;
      radiusY = scaled.tlRadiusY;
    } else if (point.dx > right - scaled.trRadiusX && point.dy < top + scaled.trRadiusY) {
      x = point.dx - right + scaled.trRadiusX;
      y = point.dy - top - scaled.trRadiusY;
      radiusX = scaled.trRadiusX;
      radiusY = scaled.trRadiusY;
    } else if (point.dx > right - scaled.brRadiusX && point.dy > bottom - scaled.brRadiusY) {
      x = point.dx - right + scaled.brRadiusX;
      y = point.dy - bottom + scaled.brRadiusY;
      radiusX = scaled.brRadiusX;
      radiusY = scaled.brRadiusY;
    } else if (point.dx < left + scaled.blRadiusX && point.dy > bottom - scaled.blRadiusY) {
      x = point.dx - left - scaled.blRadiusX;
      y = point.dy - bottom + scaled.blRadiusY;
      radiusX = scaled.blRadiusX;
      radiusY = scaled.blRadiusY;
    } else {
      return true; // inside and not within the rounded corner area
    }

    x = x / radiusX;
    y = y / radiusY;
    // check if the point is outside the unit circle
    if (x * x + y * y > 1.0.px) {
      return false;
    }
    return true;
  }

  /// See [RRect.lerp]
  static PxRRect? lerp(PxRRect? a, PxRRect? b, double t) {
    if (b == null) {
      if (a == null) {
        return null;
      } else {
        final double k = 1.0 - t;
        return PxRRect._raw(
          left: a.left * k.px,
          top: a.top * k.px,
          right: a.right * k.px,
          bottom: a.bottom * k.px,
          tlRadiusX: max(PxDouble.zero, a.tlRadiusX * k.px),
          tlRadiusY: max(PxDouble.zero, a.tlRadiusY * k.px),
          trRadiusX: max(PxDouble.zero, a.trRadiusX * k.px),
          trRadiusY: max(PxDouble.zero, a.trRadiusY * k.px),
          brRadiusX: max(PxDouble.zero, a.brRadiusX * k.px),
          brRadiusY: max(PxDouble.zero, a.brRadiusY * k.px),
          blRadiusX: max(PxDouble.zero, a.blRadiusX * k.px),
          blRadiusY: max(PxDouble.zero, a.blRadiusY * k.px),
        );
      }
    } else {
      if (a == null) {
        return PxRRect._raw(
          left: b.left * t.px,
          top: b.top * t.px,
          right: b.right * t.px,
          bottom: b.bottom * t.px,
          tlRadiusX: max(PxDouble.zero, b.tlRadiusX * t.px),
          tlRadiusY: max(PxDouble.zero, b.tlRadiusY * t.px),
          trRadiusX: max(PxDouble.zero, b.trRadiusX * t.px),
          trRadiusY: max(PxDouble.zero, b.trRadiusY * t.px),
          brRadiusX: max(PxDouble.zero, b.brRadiusX * t.px),
          brRadiusY: max(PxDouble.zero, b.brRadiusY * t.px),
          blRadiusX: max(PxDouble.zero, b.blRadiusX * t.px),
          blRadiusY: max(PxDouble.zero, b.blRadiusY * t.px),
        );
      } else {
        return PxRRect._raw(
          left: _lerpPxDouble(a.left, b.left, t),
          top: _lerpPxDouble(a.top, b.top, t),
          right: _lerpPxDouble(a.right, b.right, t),
          bottom: _lerpPxDouble(a.bottom, b.bottom, t),
          tlRadiusX: max(PxDouble.zero, _lerpPxDouble(a.tlRadiusX, b.tlRadiusX, t)),
          tlRadiusY: max(PxDouble.zero, _lerpPxDouble(a.tlRadiusY, b.tlRadiusY, t)),
          trRadiusX: max(PxDouble.zero, _lerpPxDouble(a.trRadiusX, b.trRadiusX, t)),
          trRadiusY: max(PxDouble.zero, _lerpPxDouble(a.trRadiusY, b.trRadiusY, t)),
          brRadiusX: max(PxDouble.zero, _lerpPxDouble(a.brRadiusX, b.brRadiusX, t)),
          brRadiusY: max(PxDouble.zero, _lerpPxDouble(a.brRadiusY, b.brRadiusY, t)),
          blRadiusX: max(PxDouble.zero, _lerpPxDouble(a.blRadiusX, b.blRadiusX, t)),
          blRadiusY: max(PxDouble.zero, _lerpPxDouble(a.blRadiusY, b.blRadiusY, t)),
        );
      }
    }
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    if (runtimeType != other.runtimeType) {
      return false;
    }
    return other is PxRRect &&
        other.left == left &&
        other.top == top &&
        other.right == right &&
        other.bottom == bottom &&
        other.tlRadiusX == tlRadiusX &&
        other.tlRadiusY == tlRadiusY &&
        other.trRadiusX == trRadiusX &&
        other.trRadiusY == trRadiusY &&
        other.blRadiusX == blRadiusX &&
        other.blRadiusY == blRadiusY &&
        other.brRadiusX == brRadiusX &&
        other.brRadiusY == brRadiusY;
  }

  @override
  int get hashCode => Object.hash(
        left,
        top,
        right,
        bottom,
        tlRadiusX,
        tlRadiusY,
        trRadiusX,
        trRadiusY,
        blRadiusX,
        blRadiusY,
        brRadiusX,
        brRadiusY,
      );

  @override
  String toString() {
    final String rect = '${left.toStringAsFixed(1)}, '
        '${top.toStringAsFixed(1)}, '
        '${right.toStringAsFixed(1)}, '
        '${bottom.toStringAsFixed(1)}';
    if (tlRadius == trRadius && trRadius == brRadius && brRadius == blRadius) {
      if (tlRadius.x == tlRadius.y) {
        return 'PxRRect.fromLTRBR($rect, ${tlRadius.x.toStringAsFixed(1)})';
      }
      return 'PxRRect.fromLTRBXY($rect, ${tlRadius.x.toStringAsFixed(1)}, ${tlRadius.y.toStringAsFixed(1)})';
    }
    return 'PxRRect.fromLTRBAndCorners('
        '$rect, '
        'topLeft: $tlRadius, '
        'topRight: $trRadius, '
        'bottomRight: $brRadius, '
        'bottomLeft: $blRadius'
        ')';
  }

  @override
  RRect toDp(BuildContext context) {
    return RRect.fromRectAndCorners(
      Rect.fromLTRB(left.toDp(context), top.toDp(context), right.toDp(context), bottom.toDp(context)),
      topLeft: tlRadius.toDp(context),
      topRight: trRadius.toDp(context),
      bottomRight: brRadius.toDp(context),
      bottomLeft: blRadius.toDp(context),
    );
  }
}
